package auth.config;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.Set;
import java.util.stream.Collectors;

import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.core.authority.AuthorityUtils.authorityListToSet;
import static org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames.ID_TOKEN;
import static org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL;

@Configuration(proxyBeanMethods = false)
class AuthorizationConfig {
    /**
     * @see OidcUserInfoEndpointFilter
     * @see OidcUserInfoAuthenticationProvider
     */
    @Bean
    @Order(HIGHEST_PRECEDENCE)
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        // https://docs.spring.io/spring-authorization-server/docs/current/reference/html/guides/how-to-userinfo.html
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(withDefaults()); // Enable OpenID Connect 1.0
        http.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // oidc userinfo 需要验证
        http.cors(withDefaults()); // swagger oauth2 login
        http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(
            new LoginUrlAuthenticationEntryPoint(DEFAULT_LOGIN_PAGE_URL),
            new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
        ));
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { // oauth2ResourceServer().jwt() 需要
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        // https://docs.spring.io/spring-authorization-server/docs/current/reference/html/guides/how-to-userinfo.html
        // https://github.com/spring-projects/spring-authorization-server/blob/main/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java
        return (context) -> { // 在 ID_TOKEN 或 ACCESS_TOKEN 中添加权限
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                Set<String> scope = context.getAuthorizedScopes().stream().map("SCOPE_"::concat).collect(Collectors.toSet());
                scope.addAll(authorityListToSet(context.getPrincipal().getAuthorities()));
                context.getClaims().claim("scope", scope);
            } else if (ID_TOKEN.equals(context.getTokenType().getValue())) {
                context.getClaims().claim("authorities", authorityListToSet(context.getPrincipal().getAuthorities()));
            }
        };
    }

    @Bean
    RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    // @Bean
    OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    // @Bean
    OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * keytool -genkeypair -alias oauth2 -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 365 ^
     * -keystore oauth2.p12 -storetype PKCS12 -storepass storepass ^
     * -dname "CN=jhxxb.com, OU=yf, O=xiaomi, L=wuhan, ST=hubei, C=CN"
     */
    @Bean
    JWKSource<SecurityContext> jwkSource() throws KeyStoreException, IOException, JOSEException, CertificateException, NoSuchAlgorithmException {
        String keyStorePath = "oauth2.p12"; // 密钥库文件路径
        String keyStorePassword = "storepass"; // 密钥库口令
        String keyAlias = "oauth2"; // 密钥别名
        // String keyPassword = ""; // 密钥口令

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        char[] pin = keyStorePassword.toCharArray();
        keyStore.load(new ClassPathResource(keyStorePath).getInputStream(), pin);

        RSAKey rsaKey = RSAKey.load(keyStore, keyAlias, pin);
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    @Bean
    @Profile("dev")
    ApplicationListener<ApplicationReadyEvent> registeredClients(ServerProperties serverProperties, RegisteredClientRepository registeredClientRepository) {
        Duration timeout = serverProperties.getServlet().getSession().getTimeout();
        RegisteredClient gateway = RegisteredClient.withId("gateway").clientId("gateway").clientName("gateway")
            .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
            .redirectUri("http://gateway-server:9527/login/oauth2/code/gateway-client-oidc")
            // .postLogoutRedirectUri("http://gateway-server")
            .authorizationGrantTypes(authorizationGrantTypes -> {
                authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN);
                authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);
            }).scopes(scopes -> {
                scopes.add(OidcScopes.OPENID);
                scopes.add(OidcScopes.PROFILE);
            }).clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(false) // 是否需要授权同意
                .requireProofKey(true) // 是否仅支持 PKCE 登录(PKCE 不要设置 clientSecret 且 ClientAuthenticationMethod 为 NONE)
                .build()
            ).tokenSettings(TokenSettings.builder()
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 生成 JWT token（还有 opaque token）
                .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) // 签名算法
                .accessTokenTimeToLive(timeout) // access_token 有效期
                .refreshTokenTimeToLive(timeout.plusDays(3)) // refresh_token 有效期
                .reuseRefreshTokens(true) // 是否重用刷新令牌
                .build()
            ).build();
        RegisteredClient swagger = RegisteredClient.withId("swagger").clientId("swagger").clientName("swagger")
            .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
            .redirectUri("http://gateway-server:9527/webjars/swagger-ui/oauth2-redirect.html")
            // .postLogoutRedirectUri("")
            .authorizationGrantTypes(authorizationGrantTypes -> {
                authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN);
                authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);
            }).scopes(scopes -> {
                scopes.add(OidcScopes.OPENID);
                scopes.add(OidcScopes.PROFILE);
            }).clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(false)
                .requireProofKey(true)
                .build()
            ).tokenSettings(TokenSettings.builder()
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                .accessTokenTimeToLive(timeout)
                .refreshTokenTimeToLive(timeout.plusDays(3))
                .reuseRefreshTokens(true)
                .build()
            ).build();
        RegisteredClient test = RegisteredClient.withId("test").clientId("test").clientName("test")
            .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
            .redirectUri("http://test-server:81/api/login/oauth2/code/test-client-oidc")
            // .postLogoutRedirectUri("http://test-server:81")
            .authorizationGrantTypes(authorizationGrantTypes -> {
                authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN);
                authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);
            }).scopes(scopes -> {
                scopes.add(OidcScopes.OPENID);
                scopes.add(OidcScopes.PROFILE);
            }).clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(false)
                .requireProofKey(true)
                .build()
            ).tokenSettings(TokenSettings.builder()
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                .accessTokenTimeToLive(timeout)
                .refreshTokenTimeToLive(timeout.plusDays(3))
                .reuseRefreshTokens(true)
                .build()
            ).build();
        return event -> {
            if (registeredClientRepository.findByClientId("gateway") == null) registeredClientRepository.save(gateway);
            if (registeredClientRepository.findByClientId("swagger") == null) registeredClientRepository.save(swagger);
            if (registeredClientRepository.findByClientId("test") == null) registeredClientRepository.save(test);
        };
    }
}